#!/usr/bin/env ruby
require 'rubygems'
require 'facter'

# pre-deploy hook library
module PreDeploy
  @dry_run = false
  @process_tree = nil
  @osfamily = nil
  @stop_services_regexp = %r{nova|cinder|glance|keystone|neutron|sahara|murano|ceilometer|heat|swift|apache2|httpd}

  # get regexp that selects services and processes to stop
  # @return [Regexp]
  def self.stop_services_regexp
     @stop_services_regexp
  end

  # set regexp that selects services and processes to stop
  # @param value [Regexp]
  def self.stop_services_regexp=(value)
    @stop_services_regexp = value
  end

  # get osfamily from facter
  # @return [String]
  def self.osfamily
    return @osfamily if @osfamily
    @osfamily = Facter.value 'osfamily'
  end

  # get dry run without doing anything switch
  # @return [TrueClass,FalseClass]
  def self.dry_run
    @dry_run
  end

  # set dry run without doing anything switch
  # @param value [TrueClass,FalseClass]
  def self.dry_run=(value)
    @dry_run = value
  end

  # get ps from shell command
  # @return [String]
  def self.ps
    `ps haxo pid,ppid,cmd`
  end

  # get service statu from shell command
  # @return String
  def self.services
    `service --status-all 2>&1`
  end

  # same as process_tree but reset mnemoization
  # @return [Hash<Integer => Hash<Symbol => String,Integer>>]
  def self.process_tree_with_renew
    @process_tree = nil
    self.process_tree
  end

  # build process tree from process list
  # @return [Hash<Integer => Hash<Symbol => String,Integer>>]
  def self.process_tree
    return @process_tree if @process_tree
    @process_tree = {}
    self.ps.split("\n").each do |p|
      f = p.split
      pid = f.shift.to_i
      ppid = f.shift.to_i
      cmd = f.join ' '

      # create entry for this pid if not present
      @process_tree[pid] = {
          :children => []
      } unless @process_tree.key? pid

      # fill this entry
      @process_tree[pid][:ppid] = ppid
      @process_tree[pid][:pid] = pid
      @process_tree[pid][:cmd] = cmd

      # create entry for parent process if not present
      @process_tree[ppid] = {
          :children => []
      } unless @process_tree.key? ppid

      # fill parent's children
      @process_tree[ppid][:children] << pid
    end
    @process_tree
  end

  # kill selected pid or array of them
  # @param pids [Integer,String] Pids to kill
  # @param signal [Integer,String] Which signal?
  # @param recursive [TrueClass,FalseClass] Kill children too?
  # @return [TrueClass,FalseClass] Was the signal sent? Process may still be present even on success.
  def self.kill_pids(pids, signal = 9, recursive = true)
    pids = Array pids

    pids_to_kill = pids.inject([]) do |all_pids, pid|
      pid = pid.to_i
      if recursive
        all_pids + self.get_children_pids(pid)
      else
        all_pids << pid
      end
    end

    pids_to_kill.uniq!
    pids_to_kill.sort!

    return false unless pids_to_kill.any?
    puts "Kill these pids: #{pids_to_kill.join ', '} with signal #{signal}"
    self.run "kill -#{signal} #{pids_to_kill.join ' '}"
  end

  # recursion to find all children pids
  # @return [Array<Integer>]
  def self.get_children_pids(pid)
    pid = pid.to_i
    unless self.process_tree.key? pid
      puts "No such pid: #{pid}"
      return []
    end
    self.process_tree[pid][:children].inject([pid]) do |all_children_pids, child_pid|
      all_children_pids + self.get_children_pids(child_pid)
    end
  end

  # same as services_to_stop but reset mnemoization
  # @return Array[String]
  def self.services_to_stop_with_renew
    @services_to_stop = nil
    self.services_to_stop
  end

  # find running services that should be stopped
  # uses service status and regex to filter
  # @return [Array<String>]
  def self.services_to_stop
    return @services_to_stop if @services_to_stop
    @services_to_stop = self.services.split("\n").inject([]) do |services_to_stop, service|
      fields = service.chomp.split
      running = if fields[4] == 'running...'
                  fields[0]
                elsif fields[1] == '+'
                  fields[3]
                else
                  nil
                end

      if running =~ @stop_services_regexp
        # replace wrong service name
        running = 'httpd' if running == 'httpd.event' and self.osfamily == 'RedHat'
        running = 'openstack-keystone' if running == 'keystone' and self.osfamily == 'RedHat'
        services_to_stop << running
      else
        services_to_stop
      end
    end
  end

  # stop services that match stop_services_regex
  def self.stop_services
    self.services_to_stop.each do |service|
      puts "Try to stop service: #{service}"
      self.run "service #{service} stop"
    end
  end

  # filter pids which cmd match regexp
  # @param regexp <Regexp> Search pids by this regexp
  # @return [Hash<Integer => Hash<Symbol => String,Integer>>]
  def self.pids_by_regexp(regexp)
    matched = {}
    self.process_tree.each do |pid,process|
      matched[pid] = process if process[:cmd] =~ regexp
    end
    matched
  end

  # kill pids that match stop_services_regexp
  # @return <TrueClass,FalseClass>
  def self.kill_pids_by_stop_regexp
    pids = self.pids_by_regexp(@stop_services_regexp).keys
    self.kill_pids pids
  end

  # here be other fixes
  # TODO: not needed anymore?
  def self.misc_fixes
    if self.osfamily == 'Debian'
      puts  'Enabling WSGI module'
      self.run 'a2enmod wsgi'
    end
  end

  # run the shell command with dry_run support
  # @param cmd [String] Command to run
  def self.run(cmd)
    command = "#{self.dry_run ? 'echo' : ''} #{cmd} 2>&1"
    system command
  end
end # class

if __FILE__ == $0
  # PreDeploy.dry_run = true
  PreDeploy.misc_fixes
  PreDeploy.stop_services
  PreDeploy.kill_pids_by_stop_regexp
end
